﻿using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
using System.Reflection;
using System.Collections.ObjectModel;

namespace Faml.Frontend
{
    /// <summary>
    /// Describes the Faml Expression node
    /// </summary>
    public enum FamlExpressionType
    {
        FieldAccess,
        UnaryExpression,
        BinaryExpression,
        Constant,
        NewArray,
        DefParameter,
        LambdaParameter,
        Lambda,
        FunctionCall,
        ExpressionReference,
        Dictionary, // TODO: should we rename Dictionary to Structure?
        ConditionalExpression
    }

    public abstract class FamlExpression
    {
        /// <summary>Expression type or null if type is still unknown</summary>
        private Type type = null;

        /// <summary>
        /// Makes metadata by filling the metadata field
        /// </summary>
        protected abstract void MakeMetadata();

        /// <summary> Metadata extensions </summary>
        protected FamlExpressionExtension metadata = new FamlExpressionExtension();

        /// <summary>Gets the metadata extensions.</summary>
        public FamlExpressionExtension Metadata
        {
            get { return metadata; }
        }

        /// <summary>Returns parameters the node depends on.</summary>
        public ReadOnlyCollection<FamlLambdaParameter> NodeParameters
        {
            get { return metadata.DependsOnParameters; }
        }

        private FamlExpressionClass @class;

        /// <summary>
        /// Gets or sets a class that the expression belongs to.
        /// </summary>
        public FamlExpressionClass Class
        {
            get { return @class; }
            set { @class = value; }
        }

        /// <summary>
        /// Gets the children of the expression node.
        /// </summary>
        public abstract IEnumerable<FamlExpression> Children { get; }
        
        /// <summary>Performs expansion of expression.</summary>
        /// <returns>New expression that is result of expansion</returns>
        public abstract FamlExpression Expand();

        /// <summary>Returns type of expression.</summary>
        public Type Type
        {
            get {
                if (type == null)
                    type = ComputeType();
                return type;
            }
        }

        /// <summary>Computes type of expression</summary>
        /// <returns>Type of expression</returns>
        protected abstract Type ComputeType();

        /// <summary>
        /// Type of the Faml Expression node
        /// </summary>
        public abstract FamlExpressionType NodeType { get; }
    }

    public enum UnaryOperations : byte
    {
        /// <summary>
        /// A sign for positive numbers
        /// </summary>
        Positive, 
        /// <summary>
        /// A sign for negative numbers
        /// </summary>
        Negate,
        /// <summary>
        /// Used for a node that represents an expression that has a constant value of type FamlExpression and may contain extra parameters.        
        /// </summary>
        /// <remarks>Used for LambdaExpression</remarks>
        Quote
    }

    public class FamlUnaryExpression : FamlExpression
    {
        private UnaryOperations op;
        private FamlExpression target;

        public FamlUnaryExpression(UnaryOperations op, FamlExpression target)
        {
            if (op == UnaryOperations.Quote)
                throw new ApplicationException("To construct Quote call a static MakeQuote method");

            this.op = op;
            this.target = target;
            MakeMetadata();
        }

        private FamlUnaryExpression(FamlExpression target, ReadOnlyCollection<FamlLambdaParameter> parameters)
        {
            this.op = UnaryOperations.Quote;
            this.target = target;
            MakeMetadata(parameters);
        }

        /// <summary>
        /// Creates a quote expression that depends on specified extra parameters.
        /// </summary>
        /// <param name="target">Target expression</param>
        /// <param name="parameters">Extra parameters</param>
        /// <returns>A quote expression</returns>
        public static FamlUnaryExpression MakeQuote(FamlExpression target, ReadOnlyCollection<FamlLambdaParameter> parameters)
        {
            return new FamlUnaryExpression(target, parameters);
        }

        public UnaryOperations Operator
        {
            get
            {
                return op;
            }
        }

        protected string OperatorAsString
        {
            get
            {
                switch (op)
                {
                    case UnaryOperations.Positive:
                        return "+";
                    case UnaryOperations.Negate:
                        return "-";
                    case UnaryOperations.Quote:
                        return "";
                    default:
                        return "??";
                }
            }
        }

        public FamlExpression Target
        {
            get
            {
                return target;
            }
        }

        public override FamlExpression Expand()
        {
            return new FamlUnaryExpression(op, target.Expand());
        }

        protected override Type ComputeType()
        {
            return target.Type;
        }

        public override string ToString()
        {
            if (target is FamlBinaryExpression)
            {
                return String.Format("{0}({1})", OperatorAsString, target.ToString());
            }

            return String.Format("{0}{1}", OperatorAsString, target.ToString());
        }

        protected override void MakeMetadata()
        {
            this.metadata.Height = target.Metadata.Height + 1;
            this.metadata.Hash = NodeType.GetHashCode() ^ op.GetHashCode() ^ target.Metadata.Hash;
            this.metadata.AddIfNotExist(target.NodeParameters);
        }

        private void MakeMetadata(ReadOnlyCollection<FamlLambdaParameter> parameters)
        {
            MakeMetadata();
            this.metadata.AddIfNotExist(parameters);
        }


        public override FamlExpressionType NodeType
        {
            get { return FamlExpressionType.UnaryExpression; }
        }

        public override IEnumerable<FamlExpression> Children
        {
            get { yield return target; }
        }
    }

    public abstract class FamlConstant : FamlExpression
    {
        public abstract object Value { get; }
        

        public override FamlExpressionType NodeType
        {
            get { return FamlExpressionType.Constant; }
        }

        public override IEnumerable<FamlExpression> Children
        {
            get { yield break; }
        }
    }

    public sealed class FamlConstant<T> : FamlConstant
    {
        private T v;

        public FamlConstant(T v)
        {
            this.v = v;
            MakeMetadata();
        }

        public override object Value
        {
            get
            {
                return v;
            }
        }

        public T TypedValue
        {
            get { return v; }
        }

        public override string ToString()
        {
            if (v is string)
                return String.Format("\"{0}\"", v); // TODO: What if there are some " in v?
            else if (v is double)
                return String.Format("{0:F}", v);
            else if (v is DateTime)
                return String.Format("@\"{0}\"", v);
            else if(v is TimeSpan) {
                TimeSpan ts = (TimeSpan)(object)v;
                return String.Format("@\"{0}d {1}h\"", ts.Days, ts.Hours);
            } else
                return String.Format("{0}", v);
        }

        public override FamlExpression Expand()
        {
            return new FamlConstant<T>(v);
        }

        protected override Type ComputeType()
        {
            return typeof(T);
        }

        protected override void MakeMetadata()
        {
            this.metadata.Height = 1;
            this.metadata.Hash = v.GetHashCode();
        }
    }

    public class FamlInlineArrayExpression : FamlExpression
    {
        private IList<FamlExpression> items;

        public FamlInlineArrayExpression(IList<FamlExpression> items)
        {
            this.items = items;
            MakeMetadata();
        }

        /// <summary>
        /// Gets the items of the array.
        /// </summary>
        public IList<FamlExpression> Items
        {
            get
            {
                return items;
            }
        }

        public override FamlExpression Expand()
        {
            List<FamlExpression> newItems = new List<FamlExpression>(items.Count);
            foreach (FamlExpression item in items)
                newItems.Add(item.Expand());
            return new FamlInlineArrayExpression(newItems);
        }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("{");
            for (int i = 0; i < items.Count; i++)
            {
                sb.Append(items[i]);
                if (i < items.Count - 1)
                    sb.Append(", ");
                else
                    sb.Append("}");
            }
            return sb.ToString();
        }

        protected override Type ComputeType()
        {
            Type resultType = null;
            foreach (FamlExpression expr in items)
            {
                Type itemType = expr.Type;
                if (resultType == null)
                    resultType = itemType;
                else if (resultType != itemType)
                {
                    if (resultType == typeof(int) && itemType == typeof(double))
                        resultType = typeof(double);
                    else if (resultType != typeof(double) || itemType != typeof(int))
                        throw new Exception(String.Format("Array item type {0} does not match array type {1}", itemType.ToString(), resultType.ToString()));
                }
            }
            return resultType.MakeArrayType();
        }

        protected override void MakeMetadata()
        {
            int hash = NodeType.GetHashCode();
            int height = 1;

            foreach (FamlExpression item in this.items)
            {
                height = Math.Max(height, item.Metadata.Height + 1);
                hash ^= item.Metadata.Hash;
                metadata.AddIfNotExist(item.NodeParameters);
            }

            this.metadata.Height = height;
            this.metadata.Hash = hash;
        }

        public override FamlExpressionType NodeType
        {
            get { return FamlExpressionType.NewArray; }
        }

        public override IEnumerable<FamlExpression> Children
        {
            get { return items; }
        }
    }
 
    public class FamlDefinition
    {
        private string name;
        private List<DefinitionParameter> parameters;
        private FamlExpression body;

        public FamlDefinition(string name, List<DefinitionParameter> parameters, FamlExpression body)
        {
            this.name = name;
            this.parameters = parameters;
            this.body = body;
        }

        public FamlDefinition(string name, FamlExpression body)
        {
            this.name = name;
            this.parameters = new List<DefinitionParameter>(); // Empty parameters list
            this.body = body;
        }

        public string Name
        {
            get
            {
                return name;
            }
        }

        public int ParameterCount
        {
            get
            {
                return parameters.Count;
            }
        }

        public IEnumerable<DefinitionParameter> Parameters
        {
            get
            {
                return parameters;
            }
        }

        public FamlExpression Body
        {
            get
            {
                return body;
            }
        }

        public FamlExpression Expand(List<FamlExpression> args)
        {
            if (args.Count != parameters.Count)
                throw new ArgumentException("Wrong number of arguments in expand");
            FamlExpression[] oldArgs = new FamlExpression[args.Count];
            for (int i = 0; i < parameters.Count; i++)
                oldArgs[i] = parameters[i].Value;
            for (int i = 0; i < parameters.Count; i++)
                parameters[i].Value = args[i];
            FamlExpression result = body.Expand();
            for (int i = 0; i < parameters.Count; i++)
                parameters[i].Value = oldArgs[i];
            return result;
        }
    }

    public abstract class FamlParameterExpression<T> : FamlExpression
    {
        protected T parameter;

        public FamlParameterExpression(T parameter)
        {
            this.parameter = parameter;
        }

        public T Parameter
        {
            get
            {
                return parameter;
            }
        }

        public override string ToString()
        {
            return parameter.ToString();
        }

        public override IEnumerable<FamlExpression> Children
        {
            get { yield break; }
        }
    }

    public class FamlDefinitionParameterExpression : FamlParameterExpression<DefinitionParameter>
    {
        public FamlDefinitionParameterExpression(DefinitionParameter p)
            : base(p)
        {
            MakeMetadata();
        }

        public override FamlExpression Expand()
        {
            if (parameter.Value == null)
                throw new Exception("Something wrong has happened. Missing parameter value");
            return parameter.Value;
        }

        protected override Type ComputeType()
        {
            throw new ApplicationException("Type is unknown for parameter definition");
            //return parameter.Value.Type;
        }

        protected override void MakeMetadata()
        {
            this.metadata.Height = -1; // parameter.Value.Metadata.Height + 1;
            this.metadata.Hash = NodeType.GetHashCode() ^ parameter.Name.GetHashCode(); // ^ parameter.Value.Metadata.Hash;
            //this.metadata.AddIfNotExist(parameter.Value.NodeParameters);
        }

        public override FamlExpressionType NodeType
        {
            get { return FamlExpressionType.DefParameter; }
        }
    }
    
}